function [ tags ] = read_tiff_tags( filename )
%READ_TIFF_TAGS Reads the tags from a TIFF file
%
% Returns a cell array of tags, each element of which contains a structure
% with the tags for each image in the TIFF file.
%
% Similar in behavior to MATLAB's IMFINFO, but specific to TIFF.
%
% Written by: Wade Schwartzkopf, NGA/IDT
%
% //////////////////////////////////////////
% /// CLASSIFICATION: UNCLASSIFIED       ///
% //////////////////////////////////////////

%% Define basic TIFF stuff
% These are current as of June 9, 2015.
% TIFF Image File Directory (IFD) data types
TYPE_SIZE = [1 1 2 4 8 1 1 2 4 8 4 8];
TYPE_STR = {'uint8','char', 'uint16', 'uint32', '', 'int8', 'uint8', 'int16', 'int32', '', 'single', 'double'};
% Baseline TIFF tages
BASELINE_TAG_KEYS = [254:259, 262:266, 270:274, 277:284, 288:291, 296, ...
    305:306, 315:316, 320, 338, 33432];
BASELINE_TAG_VALUES = {...
    'NewSubfileType',...
    'SubfileType',...
    'ImageWidth',...
    'ImageLength',...
    'BitsPerSample',...
    'Compression',...
    'PhotometricInterpretation',...
    'Threshholding',...
    'CellWidth',...
    'CellLength',...
    'FillOrder',...
    'ImageDescription',...
    'Make',...
    'Model',...
    'StripOffsets',...
    'Orientation',...
    'SamplesPerPixel',...
    'RowsPerStrip',...
    'StripByteCounts',...
    'MinSampleValue',...
    'MaxSampleValue',...
    'XResolution',...
    'YResolution',...
    'PlanarConfiguration',...
    'FreeOffsets',...
    'FreeByteCounts',...
    'GrayResponseUnit',...
    'GrayResponseCurve',...
    'ResolutionUnit',...
    'Software',...
    'DateTime',...
    'Artist',...
    'HostComputer',...
    'ColorMap',...
    'ExtraSamples',...
    'Copyright'};
% Extension tags
EXTENSION_TAG_KEYS = [269, 285:287, 292:293, 297, 301, 317:319, 321:328, ...
    330, 332:334, 336:337, 339:347, 351, 400:405, 433:434, 512:515, ...
    517:521, 529:532, 559, 700, 32781, 34732];
EXTENSION_TAG_VALUES = {'DocumentName', ...
    'PageName', ...
    'XPosition', ...
    'YPosition', ...
    'T4Options', ...
    'T6Options', ...
    'PageNumber', ...
    'TransferFunction', ...
    'Predictor', ...
    'WhitePoint', ...
    'PrimaryChromaticities', ...
    'HalftoneHints', ...
    'TileWidth', ...
    'TileLength', ...
    'TileOffsets', ...
    'TileByteCounts', ...
    'BadFaxLines', ...
    'CleanFaxData', ...
    'ConsecutiveBadFaxLines', ...
    'SubIFDs', ...
    'InkSet', ...
    'InkNames', ...
    'NumberOfInks', ...
    'DotRange', ...
    'TargetPrinter', ...
    'SampleFormat', ...
    'SMinSampleValue', ...
    'SMaxSampleValue', ...
    'TransferRange', ...
    'ClipPath', ...
    'XClipPathUnits', ...
    'YClipPathUnits', ...
    'Indexed', ...
    'JPEGTables', ...
    'OPIProxy', ...
    'GlobalParametersIFD', ...
    'ProfileType', ...
    'FaxProfile', ...
    'CodingMethods', ...
    'VersionYear', ...
    'ModeNumber', ...
    'Decode', ...
    'DefaultImageColor', ...
    'JPEGProc', ...
    'JPEGInterchangeFormat', ...
    'JPEGInterchangeFormatLength', ...
    'JPEGRestartInterval', ...
    'JPEGLosslessPredictors', ...
    'JPEGPointTransforms', ...
    'JPEGQTables', ...
    'JPEGDCTables', ...
    'JPEGACTables', ...
    'YCbCrCoefficients', ...
    'YCbCrSubSampling', ...
    'YCbCrPositioning', ...
    'ReferenceBlackWhite', ...
    'StripRowCounts', ...
    'XMP', ...
    'ImageID', ...
    'ImageLayer'};
% Tags used for GeoTIFF
GEOTIFF_TAG_KEYS = [33550, 33922, 34264, 34735:34737];
GEOTIFF_TAG_VALUES = {'ModelPixelScaleTag',...
    'ModelTiepointTag',...
    'ModelTransformationTag',...
    'GeoKeyDirectoryTag',...
    'GeoDoubleParamsTag',...
    'GeoAsciiParamsTag'};
% Consolidate all tags that we recognize
RECOGNIZED_TAG_KEYS = [BASELINE_TAG_KEYS EXTENSION_TAG_KEYS GEOTIFF_TAG_KEYS];
RECOGNIZED_TAG_VALUES = [BASELINE_TAG_VALUES EXTENSION_TAG_VALUES GEOTIFF_TAG_VALUES];
RECOGNIZED_TAGS = containers.Map(RECOGNIZED_TAG_KEYS, RECOGNIZED_TAG_VALUES);


%% Read all TIFF metadata
% Determine TIFF endianness
fid = fopen(filename,'r');
endian = fread(fid,2,'uint8=>char').';
fclose(fid);
switch endian
    case 'II'
        endian = 'l';
    case 'MM'
        endian = 'b';
    otherwise
        error('READ_TIFF_TAGS:UNRECOGNIZED_ENDIANNESS','TIFF endian values are limited to II or MM.');
end
% Read TIFF Image File Headers
fid = fopen(filename,'r',endian); % Reopen file with correct endianness
fseek(fid,2,'bof'); % Already read endianness, so we can now skip over it
magicnumber = fread(fid,1,'int16'); % This value should be 42.
next_ifd = fread(fid,1,'uint32'); % Pointer to next image file header
tags = {};
while next_ifd > 0 % For every image in file, read its tags
    fseek(fid, next_ifd, 'bof');
    [tags{end+1}, next_ifd] = read_ifd(fid);
end
fclose(fid);

% Display some metadata from first image in file for debugging purposes.
% for i = 1:numel(tags), disp(tags{i}); end;
% disp(imfinfo(filename));


    %% Read a TIFF Image File Directory (IFD)
    % Assumes file handle is pointed at beginning of an IFD
    function [tags, next_ifd] = read_ifd(fid)
        tags = struct();
        num_ifd_entries = fread(fid,1,'uint16');
        for entry_i = 1:num_ifd_entries
            tag_numeric = fread(fid,1,'uint16');
            type = fread(fid,1,'uint16');
            count = fread(fid,1,'uint32');
            if type>0 && type<=numel(TYPE_SIZE) && ~isempty(TYPE_STR{type})
                total_size = TYPE_SIZE(type)*count;
                if total_size <= 4 % offset_value is a value
                    value = fread(fid,count,['*' TYPE_STR{type}]);
                    fseek(fid,4-total_size,'cof');
                else % offset_value is an offset
                    offset = fread(fid,1,'*uint32');
                    save_ptr = ftell(fid);
                    fseek(fid,offset,'bof');
                    value = fread(fid,count,['*' TYPE_STR{type}]).';
                    fseek(fid,save_ptr,'bof');
                end
                if isKey(RECOGNIZED_TAGS, tag_numeric);
                    tags.(RECOGNIZED_TAGS(tag_numeric)) = value;
                else
                    if ~isfield(tags,'Unrecognized')
                        tags.Unrecognized.tag = tag_numeric;
                        tags.Unrecognized.value = value;
                    else
                        tags.Unrecognized(end+1).tag = tag_numeric;
                        tags.Unrecognized(end).value = value;
                    end
                end
            else
                % Most notably, this code won't handle the XResolution and
                % YResolution tags, which are of type RATIONAL, but are
                % required is many TIFFs.
                % warning('READ_TIFF_TAGS:UNRECOGNIZED_TIFF_DATATYPE', ...
                %     ['Unrecognized TIFF datatype.  Skipping tag ' num2str(tag_numeric) '.']);
                fseek(fid,4,'cof');
            end
        end
        next_ifd = fread(fid,1,'uint32');
    end

end

% //////////////////////////////////////////
% /// CLASSIFICATION: UNCLASSIFIED       ///
% //////////////////////////////////////////